from __future__ import with_statement
from __future__ import division

import os
import collections
import random
import itertools

# Ingame we have many values that are interdependent.
# Maybe use Cellulose 0.2 to keep them accurate.

class LogMaster( object ):
    def __init__( self, stream=None ):
        self.on = None
        self._logger = None
        self.write = None
        if stream:
            self.redirect( stream )
        else:
            self.redirect( os.sys.stdout )
    
    def redirect( self, stream=None ):
        if stream:
            self._logger = stream
            self.write = self._logger.write
            self.on = True
        else:
            self._logger = None
            self.write = lambda x:None
            self.on = False
    
    def __nonzero__( self ):
        return self.on
    # See! This is what I tend to waste my time on when programming.
    # A whole new pointless class, just so we can redirect the logging 
    # when the program is running.

global_log = LogMaster()

def format_HP( unit ):
    return '%1.1f/%d'%( unit.HP, unit.HPmax )

class Entity( object ):
    logging = global_log

class Unit( Entity ):
    def __init__( self, name, strength, toughness, dexterity, craft, geist, **kwargs ):
        if self.logging:
            self.logging.write( 'Creating unit "%s".\n' % name )
        self.name = name
        self.strength = strength
        self.toughness = toughness
        self.dexterity = dexterity
        self.craft = craft
        self.geist_max = geist
        self.geist = geist
        self.geist_recovery = 5
        self.size = kwargs.get( 'size', 10 )
        self.combat = self.dexterity
        self.HPmax = 1
        self.HP = 1
        self.armour = None
        self._damage_mods = [ ( 'sharp', 4/3 ) ]
        self.don_armour( kwargs.get( 'armour', None ) )
        self.fortification = 0
        self.attacks = []
        self.equip_weapon( 'fists' )
        if 'weapons' in kwargs:
            weapons = kwargs['weapons']
            if isinstance( weapons, basestring ):
                self.equip_weapon( weapons )
            else:
                for weapon in weapons:
                    self.equip_weapon( weapon )
        if self.logging:
            self.logging.write( 'Created unit ' )
            self.logging.write( str( self ) )
            self.logging.write( '\n' )
                    
    def don_armour( self, armour ):
        if armour is None:
            armour = Armour( 1 )
        injuredness = self.HP/self.HPmax
        if self.armour: #remove old resistances
            for damage_mod in self.armour.damage_mods:
                self._damage_mods.remove( damage_mod )
        self.armour = armour
        self.HPmax = int( self.toughness * self.size * self.armour.HP_multiplier *( 30/100 ) // 1 )
        self.HP = int( self.HPmax * injuredness // 1 )
        # I am in two minds about coercing HP, and any other values, to int.
        if self.armour:
            self._damage_mods.extend( armour.damage_mods )
        if self.logging:
            self.logging.write( '%s equipped armour. It now has %s HP.\n' % ( self.name, format_HP( self ) ) )
    
    def equip_weapon( self, weapon_type ):
        self.attacks.append( Weapon( self, weapon_type ) )
        if self.logging:
            self.logging.write( 'Unit ' )
            self.logging.write( self.name )
            self.logging.write( ' equipped a ' )
            self.logging.write( weapon_type )
            self.logging.write( '.\n' )

    def fortify( self ):
        self.fortification = min( self.fortification+self.craft/3, self.craft )
        if self.logging:
            self.logging.write( '%s has fortified itself. It now has %d fortification.' % ( self.name, self.fortification ) )

    def attack( self, target, weapon=None ):
        if self: # cant attack if you're dead
            geist_attack_cost = 15*self.geist_mod
            if self.geist >= geist_attack_cost: # Actually, you shouldn't get the option to attack if you can't pay, I'm just makin' it easy for myself.
                self.geist -= geist_attack_cost
                if not weapon:
                    attacks = sorted( self.attacks, key=lambda weapon: weapon.evaluate( target ) )
                    weapon = attacks[-1]
                unit_attacks( self, target, weapon )
            else:
                if self.logging:
                    self.logging.write( self.name )
                    self.logging.write( " tried to attack, but couldn't pay the %d geist required.\n" % geist_attack_cost )
        else:
            if self.logging:
                self.logging.write( self.name )
                self.logging.write( ' tried to attack, but was dead.\n' )
                
    def retaliate( self, target ):
        geist_retaliation_cost = 15*self.geist_mod
        if self.geist >= geist_retaliation_cost:
            self.geist -= geist_retaliation_cost
            attacks = sorted( self.attacks, key=lambda weapon: weapon.evaluate( target ) )
            weapon = attacks[-1]
        else:
            weapon = None
            if self.logging:
                self.logging.write( self.name )
                self.logging.write( " couldn't retaliate, not enough geist.\n" )
        return weapon
    
    def take_damage( self, magnitude, damage_type ):
        multiplier = 1
        for mod_type, mod in self._damage_mods:
            if mod_type==damage_type:
                multiplier *= mod
        damage = magnitude * multiplier
        if self.fortification:
            self.fortification -= damage
            if self.fortification < 0:
                damage = self.fortification
                self.fortification = 0
            else:
                damage = 0
        if damage:
            self.HP -= damage
        if self.logging:
            if self:
                self.logging.write( '%s took %d %s damage. It now has %s HP.\n' % ( self.name, damage, damage_type, format_HP( self ) ) )
            else:
                self.logging.write( '%s took %d %s damage and died from the attack\n' % ( self.name, damage, damage_type ) )
            

    def __str__( self ):
        data = []
        data.append( self.name )
        data.append( 'HP:' )
        data.append( format_HP( self ) )
        data.append( ' geist: %d/%d' % ( self.geist, self.geist_max ) )
        data.append( 'Combat: %d'%self.combat )
        return ' '.join( data )

    def __nonzero__( self ):
        #I figure the Unit is True if alive, False if not. Even if we chose to have a
        #seperate representation of alive and deadness, I think this is still a useful
        #convention.
        return self.HP>0
    
    @property
    def geist_mod( self ):
        return ( self.HPmax/self.HP )**( .5 ) # self.HPmax/self.HP ws too much
    
    def change_geist( self, condition, magnitude=0 ):
        try:
            geist_gain = geist_events[condition]( magnitude )
        except TypeError:
            geist_gain = geist_events[condition]
        self.geist = min( self.geist + geist_gain, self.geist_max )
        if self.logging:
            gain_loss = 'gained' if geist_gain > 0 else 'lost'
            self.logging.write( '%(name)s %(gain_loss)s %(geist_change)d geist from a %(event)s. It now has %(geist)d/%(geistmax)d geist.\n' % {'name':self.name, 'gain_loss':gain_loss, 'geist_change':abs( geist_gain ), 'event':condition, 'geist':self.geist, 'geistmax':self.geist_max } )


geist_events = {'major victory':10, 
                'minor victory':5, 
                'undecided':0, 
                'minor defeat':-5, 
                'major defeat':-10, 
                'unopposed': lambda magnitude: random.randint( -10, 10 ) + magnitude, 
                'new turn': lambda magnitude: magnitude, 
                }

class Armour( object ):
    def __init__( self, HP_multiplier, damage_mods=None ):
        self.HP_multiplier = HP_multiplier
        if damage_mods is None:
            self.damage_mods = []
        else:
            self.damage_mods = damage_mods

weapons = collections.defaultdict( lambda:( 'blunt', 1, 1 ) )
weapons.update( {'fists': ( 'blunt', .7, .7 ), 
                'sword': ( 'sharp', 1, 1 ), 
                'axe': ( 'sharp', .8, 1.2 ), 
                'dagger': ( 'sharp', .8, .8 ), 
                'pick': ( 'impaling', .7, .7 ), 
                'pitch fork': ( 'sharp', .9, .9 ), 
                } )

damage_type_bonus = {'blunt':1, 'sharp':1, 'heat':1, 
                     'frost':1.5, 'lightning':1.5, 'impaling':1.5, 
                     'holy':.5, 'silver':.5, 'devastating':.5 }

class Weapon( object ):
    def __init__( self, wielder, weapon_type='fists' ):
        self.weapon_type = weapon_type
        self.damage_type, min_bonus, max_bonus = weapons[weapon_type]
        type_bonus = damage_type_bonus[self.damage_type]
        self.min_damage = int( ( wielder.strength + wielder.craft ) * min_bonus * type_bonus *( 3/20 )//1 )
        self.max_damage = int( wielder.strength * max_bonus * type_bonus * ( 7/10 ) // 1 )
        self.combat_mod = 0

    def evaluate( self, target ):
        '''
        Calculate a rough value of how well this attack works agains the target.
        '''
        return self.min_damage + self.max_damage
        # Simle valuation, just the total damage with no regard for the target.
        
    def __str__( self ):
        #return '%s, %d-%d %s damage' % ( self.weapon_type, self.min_damage, self.max_damage, self.damage_type )
        return '%(weapon_type)s, %(min_damage)d-%(max_damage)d %(damage_type)s damage' % self.__dict__

def roll( a, b ):
    return a - b + random.randint( -10, 10 )  > 0

_victory_types = {( True, False ):'major victory', 
                  ( True, True ):'minor victory', 
                  ( False, False ):'minor defeat', 
                  ( False, True ):'major defeat' }

def unit_attacks( attacker, defender, weapon ):
    attacker_won = roll( attacker.combat + weapon.combat_mod, defender.combat )
    defender_damage = weapon.max_damage if attacker_won else weapon.min_damage
    defender_weapon = defender.retaliate( attacker )
    if defender_weapon:
        defender_won = roll( defender.combat + defender_weapon.combat_mod, attacker.combat )
        attacker_damage = defender_weapon.max_damage if defender_won else defender_weapon.min_damage

    defender.take_damage( defender_damage, weapon.damage_type )
    if defender_weapon:
        attacker.take_damage( attacker_damage, defender_weapon.damage_type )
        attacker.change_geist( _victory_types[ ( attacker_won, defender_won ) ] )
        defender.change_geist( _victory_types[ ( defender_won, attacker_won ) ] )
    else:
        attacker.change_geist( 'unopposed' )


if __name__ == '__main__':
    platemail = Armour( 2, [( 'sharp', 3/4 )] )
    # name, strength, toughness, dexterity, craft, geist
    GT = Unit( "God's Toenails", 10, 10, 10, 10, 60, weapons='sword' )
    SW = Unit( "Steady Wobblers", 10, 10, 10, 10, 25, weapons='pitch fork' )
    
    GT.don_armour( platemail )

    side1 = [ SW ] # side1 gets the first attack
    side2 = [ GT ]

    if global_log:
        global_log.write( '\nSide 1:\n' )
        for unit in side1:
            global_log.write( str( unit ) )
            global_log.write( ' wields:\n ' )
            global_log.write( '\n '.join( str( weapon ) for weapon in unit.attacks ) )
            global_log.write( '\n' )
        global_log.write( '\nSide 2:\n' )
        for unit in side2:
            global_log.write( str( unit ) )
            global_log.write( ' wields:\n ' )
            global_log.write( '\n '.join( str( weapon ) for weapon in unit.attacks ) )
            global_log.write( '\n' )
            
    turn = 0
    while any( side1 ) and any( side2 ):
        turn += 1
        if global_log:
            global_log.write( '\nStart of turn %d.\n' % turn )
            for unit in itertools.chain( side1, side2 ):
                global_log.write( str( unit ) )
                global_log.write( '\n' )
        
        attackers = sorted( side1, key=lambda dummy: random.random() )
        while attackers:
            attacker = attackers.pop()
            potential_targets = [unit for unit in side2 if unit]
            attacker.attack( random.choice( potential_targets ) )

        attackers = sorted( side2, key=lambda dummy: random.random() )
        while attackers:
            attacker = attackers.pop()
            potential_targets = [unit for unit in side1 if unit]
            attacker.attack( random.choice( potential_targets ) )
        
        # recover geist
        for unit in itertools.chain( side1, side2 ):
            unit.change_geist( 'new turn', unit.geist_recovery )


    if global_log:
        if any( side1 ):
            global_log.write( '\nSide 1 won.\n' )
        elif any( side2 ):
            global_log.write( '\nSide 2 won.\n' )
        else:
            global_log.write( '\nEveryone died, huzzah!\n' )


